# -*- coding: utf-8 -*-
# @Time    : 2024/6/12 15:01
# @Author  : yujiahao
# @File    : 17_generator.py
# @description: Python中的生成器

# todo  一、生成器
'''
【初识生成器】
    什么是生成器？这个概念比较模糊，各种文献都有不同的理解，但是核心基本相同。【迭代器】就是一种特殊类型的迭代器，使用 yield 关键字来生成值。生成器在每次调用时返回一个值，并在需要时暂停和恢复执行。这使得生成器在处理大型数据集或无限序列时非常高效，因为它们不会一次性生成所有值，而是按需生成。
    在python社区中，大多数时候都把迭代器和生成器是做同一个概念。不是相同么？为什么还要创建生成器？生成器和迭代器也有不同，唯一的不同就是：迭代器都是Python给你提供的已经写好的工具或者通过数据转化得来的，（比如文件句柄，iter([1,2,3])。
    生成器是需要我们自己用python代码构建的工具。最大的区别也就如此了。

【生成器的特点】
    惰性求值：生成器在需要时生成值，而不是一次性生成所有值，这使得它们在处理大型数据集时非常高效。
    状态保持：生成器函数在每次 yield 时会记住它的状态（包括局部变量和执行位置），并在下次调用时从上次暂停的地方继续执行。
    节省内存：由于生成器按需生成值，它们通常比列表等数据结构更节省内存。

【生成器的构建方式】

　　1. 通过生成器函数

　　2. 通过生成器推导式

　　3. python内置函数或者模块提供（其实1,3两种本质上差不多，都是通过函数的形式生成，只不过1是自己写的生成器函数，3是python提供的生成器函数而已）


        1> 生成器函数

        生成器函数使用 yield 关键字来逐步生成值。每次调用生成器的 __next__() 方法（或使用 next() 函数）时，生成器函数会继续执行直到遇到下一个 yield 语句，然后返回该值并暂停执行。

        示例：

        def my_generator():
            yield 1
            yield 2
            yield 3

        gen = my_generator()

        print(next(gen))  # 输出: 1
        print(next(gen))  # 输出: 2
        print(next(gen))  # 输出: 3
        # 如果再调用 next(gen)，会引发 StopIteration 异常

        2> 生成器表达式
        生成器表达式类似于列表推导式，但它们使用圆括号而不是方括号，并且是惰性求值的。

        示例：

        gen_expr = (x * x for x in range(10))

        for value in gen_expr:
            print(value)

【高级用法】
    生成器还可以使用 send、throw 和 close 方法来实现更复杂的功能。

【使用 send 方法】
    生成器不仅可以通过 next() 方法获取下一个值，还可以通过 send() 方法向生成器发送值，并且生成器可以接收这些值。

    示例：

    def generator_with_send():
        while True:
            value = (yield)
            print(f'Received: {value}')

    gen = generator_with_send()
    next(gen)  # 启动生成器
    gen.send('Hello')  # 输出: Received: Hello
    gen.send('World')  # 输出: Received: World

【使用 throw 方法】
    throw 方法可以用于在生成器内部引发异常。

    示例：

    def generator_with_throw():
        try:
            while True:
                yield
        except ValueError:
            print('ValueError caught!')

    gen = generator_with_throw()
    next(gen)  # 启动生成器
    gen.throw(ValueError)  # 输出: ValueError caught!

【使用 close 方法】
    close 方法可以用于关闭生成器，生成器会引发 GeneratorExit 异常。

    示例：

    def generator_with_close():
        try:
            while True:
                yield
        except GeneratorExit:
            print('Generator closed!')

    gen = generator_with_close()
    next(gen)  # 启动生成器
    gen.close()  # 输出: Generator closed!

【注意事项】
    - 生成器一旦耗尽（即引发 StopIteration 异常），便不能再次使用，需要重新创建生成器对象。
    - 在使用生成器时，确保正确处理 StopIteration 异常，以避免程序崩溃。
    - 使用 send、throw 和 close 方法时，确保生成器已启动（即至少调用过一次 next() 方法）。

生成器是一种强大且灵活的工具，适用于处理大型数据集和无限序列，能简化代码并提高效率。

'''


# 1.1、生成器函数

# 我们先来研究通过生成器函数构建生成器。

# 首先,我们先看一个很简单的函数:

def func():
    print(11)
    return 22
ret = func()
print(ret)

# 将函数中的return换成yield，这样func就不是函数了，而是一个生成器函数
def  fun():
    print(11)
    yield 22

ss = fun()
print(ss)

'''当我们调用函数的时候函数体里的代码会进行执行当执行到yield的关键字的时候,发现我们是想声明一个生成器.程序就会返回一个生成器给咱们
生成器的本质是迭代器，所以可以直接调用next()取值,有一点要注意：当程序运行完最后一个yield,那么后面继续运行next()程序会报错，一个yield对应一个next，next超过yield数量，就会报错，与迭代器一样。


'''


def func():
    print("111")
    yield 222
gener = func()  # 这个时候函数不会执⾏. ⽽是获取到⽣成器
ret = gener.__next__()  # 这个时候函数才会执⾏
# ret2 = gener.__next__()  # 最后⼀个yield执⾏完毕. 再次__next__()程序报错：StopIteration
print(ret)  # 并且yield会将func生产出来的数据 222 给了 ret。


#1.2、 yield与return的区别：

''' 
return一般在函数中只设置一个，他的作用是终止函数，并且给函数的执行者返回值。
yield在生成器函数中可设置多个，他并不会终止函数，next会获取对应yield生成的元素。
    1注意事项
    - 生成器一旦耗尽（即引发 StopIteration 异常），便不能再次使用，需要重新创建生成器对象。
    - 在使用生成器时，确保正确处理 StopIteration 异常，以避免程序崩溃。
    - 使用 send、throw 和 close 方法时，确保生成器已启动（即至少调用过一次 next() 方法）。
    - 生成器是一种强大且灵活的工具，适用于处理大型数据集和无限序列，能简化代码并提高效率。

这两者的区别:

 - 第一种是直接把包子全部做出来，占用内存。

 - 第二种是吃一个生产一个，非常的节省内存，而且还可以保留上次的位置。

'''

# 举例：

'''我们来看一下这个需求：老男孩向楼下卖包子的老板订购了10000个包子.包子铺老板非常实在，一下就全部都做出来了　'''

def eat():

    lst = []

    for i in range(1,10000):

        lst.append('包子'+str(i))

    return lst

e = eat()

print(e)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# 用生成器
'''但是我们由于学生没有那么多，只吃了2000个左右，剩下的8000个，就只能占着一定的空间，放在一边了。如果包子铺老板效率够高，我吃一个包子，你做一个包子，那么这就不会占用太多空间存储了，完美。'''


def eat():
    for i in range(1, 10000):
        yield '包子' + str(i)


e = eat()

for i in range(200):
    next(e)

# 多次next包子的号码是按照顺序记录的。
for i in range(300):
    next(e)



# 1.3、使用 send 方法
'''生成器不仅可以通过 next() 方法获取下一个值，还可以通过 send() 方法向生成器发送值，并且生成器可以接收这些值。

send和next()区别:

    相同点：

        send 和 next()都可以让生成器对应的yield向下执行一次。
    
        都可以获取到yield生成的值。

    不同点：

        第一次获取yield值只能用next不能用send（可以用send(None)）。
    
        send可以给上一个yield置传递值。

'''

# 示例：

def generator_with_send():
    while True:
        value = (yield)
        print(f'Received: {value}')

gen = generator_with_send()
next(gen)  # 启动生成器
gen.send('Hello')  # 输出: Received: Hello
gen.send('World')  # 输出: Received: World


# 1.4、使用 throw 方法
# throw 方法可以用于在生成器内部引发异常。

# 示例：

# def generator_with_throw():
#     try:
#         while True:
#             yield
#     except ValueError:
#         print('ValueError caught!')
#
# geng = generator_with_throw()
# next(geng)  # 启动生成器
# geng.throw(ValueError)  # 输出: ValueError caught!




# 1.5、使用 close 方法
# close 方法可以用于关闭生成器，生成器会引发 GeneratorExit 异常。

# 示例：

def generator_with_close():
    try:
        while True:
            yield
    except GeneratorExit:
        print('Generator closed!')

gen = generator_with_close()
next(gen)  # 启动生成器
gen.close()  # 输出: Generator closed!

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')


# 1.6、yield from

'''在python3中提供一种可以直接把可迭代对象中的每一个数据作为生成器的结果进行返回

    yield from ['卫龙','老冰棍','北冰洋','牛羊配'] 
        等同于：
            yield '卫龙'
            yield '老冰棍'
            yield '北冰洋'
            yield '牛羊配
'''

# 对比yield 与 yield from
def func():
    lst = ['卫龙','老冰棍','北冰洋','牛羊配']
    yield lst
g = func()
print(g)
print(next(g))  # 只是返回一个列表

def func():
    lst = ['卫龙','老冰棍','北冰洋','牛羊配']
    yield from lst
g = func()
print(g)
# 他会将这个可迭代对象(列表)的每个元素当成迭代器的每个结果进行返回。
print(next(g))
print(next(g))
print(next(g))
print(next(g))

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

#使用yield是 返回的结果是将第一个列表的元素全部返回后,在返回第二个列表
def func():
    lst1 = ['卫龙', '老冰棍', '北冰洋', '牛羊配']
    lst2 = ['馒头', '花卷', '豆包', '大饼']
    yield from lst1
    yield from lst2


g = func()
for i in g:
    print(i)

print('''\n-----------------------------------☆☆☆分割线☆☆☆----------------------------------------\n''')

# todo 二、生成器推导式
'''
我们前面了解过列表推导式，生成器表达式和列表推导式的语法上一模一样,只是把[]换成()就行了。那么跟生成器推导式有啥区别呢？
    1、列表推导式比较耗内存,所有数据一次性加载到内存。而生成器表达式遵循迭代器协议，逐个产生元素。
    2、得到的值不一样,列表推导式得到的是一个列表.生成器表达式获取的是一个生成器
    3、列表推导式一目了然，生成器表达式只是一个内存地址。

无论是生成器表达式，还是列表推导式，他只是Python给你提供了一个相对简单的构造方式，因为使用推导式非常简单，
所以大多数都会为之着迷，这个一定要慎重，推导式只能构建相对复杂的并且有规律的对象，对于没有什么规律，而且嵌套层数比较多（for循环超过三层）这样就不建议大家用推导式构建。

生成器的惰性机制: 生成器只有在访问的时候才取值,说白了.你找他要才给你值.不找他要.他是不会执行的.
'''

# 比如将十以内所有数的平方放到一个生成器表达式中

gen = (i**2 for i in range(10))
print(gen)

# 生成器表达式也可以进行筛选

# 获取1-100内能被3整除的数
gen = (i for i in range(1,100) if i % 3 == 0)
for num in gen:
    print(num)





#  其他相关的推导式（了解）
# 1、字典推导式
lst1 = ['nihao','niyehao']
lst2 = ['周杰伦','林俊杰']
dic = {lst1[i]:lst2[i] for i in range(len(lst1))}
print(dic)

# 2、集合推导式
'''集合推导式可以帮我们直接生成一个集合,集合的特点;无序,不重复 所以集合推导式自带去重功能'''
lst = [1,2,3,-1,-3,-7,9]
# 这行代码使用集合推导式来创建一个新的集合 s，其中包含 lst 中每个元素的绝对值。abs(i) 函数返回 i 的绝对值。
s = {abs(i) for i in lst}
print(s)